﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using System.Globalization;
using System.IO;

namespace QuantumFractals
{
    public enum FractalTypes { IteratedFunctionSystem }

    public struct QPoint
    {
        public double X, Y, Z;

        public QPoint(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }

    public class QFractal
    {
        public QPoint CurrentPoint { get { return currentPoint; } }
        public double Alpha { get { return alpha; } }
        public string FormulaAlpha { get { return formulaAlpha; } }
        public QPoint[] Detectors { get { return detectors; } }
        public string[] FormulaDetectors { get { return formulaDetectors; } }
        public Dictionary<string, string> FormulaConstants { get { return formulaConstants; } }
        public string Name { get { return name; } }
        public string Description { get { return description; } }
        public string CreationDate { get { return creationDate; } }
        public string Author { get { return author; } }

        private NumberFormatInfo englishNumber = new CultureInfo("en-US", false).NumberFormat;
        private string type = "Iterated Function System";
        private double alpha;
        private string formulaAlpha;
        private QPoint[] detectors; // Detector Points
        private string[] formulaDetectors;
        private Dictionary<string, string> formulaConstants;
        private double[] counter; // Detect counter;
        private QPoint currentPoint; // Dim 3, Fractal point
        private Random random;
        private double n1; // Temporary variable
        private double alpha12; // Temporary variable

        private string name;
        private string description;
        private string author;
        private string creationDate;

        public QFractal(string fileName)
        {
            LoadFromXml(fileName);
            InitDetectors(formulaAlpha, formulaDetectors, formulaConstants);
            Clear();
        }

        public QFractal(string newFormulaAlpha, string[] newFormulaDetectors, 
            Dictionary<string, string> newFormulaConstants)
        {
            InitProperties("New", "", Environment.UserName, DateTime.Today.ToShortDateString());
            InitDetectors(newFormulaAlpha, newFormulaDetectors, newFormulaConstants);
            Clear();
        }

        public QFractal(string newFormulaAlpha, string[] newFormulaDetectors,
            Dictionary<string, string> newFormulaConstants, string newName, string newDescription,
            string newAuthor, string newCreationDate)
        {
            InitProperties(newName, newDescription, newAuthor, newCreationDate);
            InitDetectors(newFormulaAlpha, newFormulaDetectors, newFormulaConstants);
            Clear();
        }

        public QFractal(double newAlpha, QPoint[] newDetectors)
        {
            InitProperties("New", "", Environment.UserName, DateTime.Today.ToShortDateString());
            InitDetectors(newAlpha, newDetectors);
            Clear();
        }

        public QFractal(double newAlpha, QPoint[] newDetectors, string newName, string newDescription,
            string newAuthor, string newCreationDate)
        {
            InitProperties(newName, newDescription, newAuthor, newCreationDate);
            InitDetectors(newAlpha, newDetectors);
            Clear();
        }

        private void InitDetectors(string newFormulaAlpha, string[] newFormulaDetectors, 
            Dictionary<string, string> newFormulaConstants)
        {
            formulaAlpha = newFormulaAlpha;
            formulaDetectors = newFormulaDetectors;
            formulaConstants = newFormulaConstants;
            alpha = Evaluator.CalculateFormula(formulaAlpha, newFormulaConstants);
            detectors = Evaluator.CalculateFormulaPoints(newFormulaDetectors, newFormulaConstants);
        }

        private void InitDetectors(double newAlpha, QPoint[] newDetectors)
        {
            alpha = newAlpha;
            detectors = newDetectors;
            formulaAlpha = newAlpha.ToString(englishNumber);
            formulaDetectors = new string[detectors.Length];
            formulaConstants = new Dictionary<string, string>();
            for (int i = 0; i < detectors.Length; i++)
            {
                formulaDetectors[i] = detectors[i].X.ToString(englishNumber) + ", " +
                    detectors[i].Y.ToString(englishNumber) + ", " +
                    detectors[i].Z.ToString(englishNumber);
            }
        }

        private void InitProperties(string newName, string newDescription,
            string newAuthor, string newCreationDate)
        {
            name = newName;
            description = newDescription;
            author = newAuthor;
            creationDate = newCreationDate;
        }

        public void Clear()
        {
            counter = new double[detectors.Length];
            currentPoint = new QPoint(0, 0, 0);

            random = new Random();
            currentPoint.X = random.NextDouble() - 0.5;
            currentPoint.Y = random.NextDouble() - 0.5;
            currentPoint.Z = random.NextDouble() - 0.5;
            double sum = Math.Sqrt(prod(currentPoint, currentPoint));
            currentPoint.X /= sum;
            currentPoint.Y /= sum;
            currentPoint.Z /= sum;
            n1 = 1.0 / (double)detectors.Length;
            alpha12 = 2 * alpha / (detectors.Length * (1 + alpha * alpha));
        }

        public QPoint CalculateNextPoint()
        {

            double[] p = new double[detectors.Length];
            for (int i = 0; i < p.Length; i++)
                p[i] = n1 + alpha12 * prod(detectors[i], currentPoint);

            double r = random.NextDouble();

            double ptmp = 0;
            QPoint detector = new QPoint(0, 0, 0);
            for (int i = 0; i < p.Length; i++)
            {
                ptmp += p[i];

                if (r <= ptmp)
                {
                    detector = detectors[i];
                    counter[i]++;
                    break;
                }
            }
            if (detector.X == 0 && detector.Y == 0 && detector.Z == 0)
                throw new Exception("Detector is empty (X=Y=Z=0)");

            double sc = prod(currentPoint, detector);
            currentPoint.X = (1 - alpha * alpha) * currentPoint.X + 2 * alpha * (1 + alpha * sc) * detector.X;
            currentPoint.Y = (1 - alpha * alpha) * currentPoint.Y + 2 * alpha * (1 + alpha * sc) * detector.Y;
            currentPoint.Z = (1 - alpha * alpha) * currentPoint.Z + 2 * alpha * (1 + alpha * sc) * detector.Z;
            double norm = Math.Sqrt(prod(currentPoint, currentPoint));
            currentPoint.X /= norm;
            currentPoint.Y /= norm;
            currentPoint.Z /= norm;
            return currentPoint;
        }

        private double prod(QPoint v1, QPoint v2)
        {
            return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;
        }

        private void LoadFromXml(string[] xml)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml.ToString());
            LoadFromXml(doc);
        }

        private void LoadFromXml(string fileName)
        {
            XmlDocument doc = new XmlDocument();
            if (File.Exists(fileName))
                doc.Load(fileName);
            else
                doc.LoadXml(fileName);
            LoadFromXml(doc);
        }

        private void LoadFromXml(XmlDocument doc)
        {
            XmlElement root = doc.DocumentElement;
            if (root.Name != "quantumfractal")
                throw new Exception("Bad xml file");
            if (root.Attributes["version"].Value != "2")
                throw new Exception("Too old xml file. We need version 2.");
            
            foreach (XmlNode mainNode in root.ChildNodes)
            {
                if (mainNode.NodeType == XmlNodeType.Element && mainNode.Name == "properties")
                {
                    foreach (XmlNode propNode in mainNode.ChildNodes)
                    {
                        if (propNode.Name == "name")
                            name = propNode.FirstChild.Value;
                        else if (propNode.Name == "description" && propNode.FirstChild != null) 
                            description = propNode.FirstChild.Value;
                        else if (propNode.Name == "author" && propNode.FirstChild != null)
                            author = propNode.FirstChild.Value;
                        else if (propNode.Name == "creationdate" && propNode.FirstChild != null)
                            creationDate = propNode.FirstChild.Value;
                        else if (propNode.Name == "alpha")
                            formulaAlpha = propNode.FirstChild.Value;
                        else if (propNode.Name == "constants")
                        {
                            formulaConstants = new Dictionary<string, string>();
                            foreach (XmlNode constNode in propNode)
                            {
                                if (constNode.Name == "formula")
                                {
                                    string[] s = constNode.FirstChild.Value.Split('=');
                                    if (s.Length != 2)
                                        throw new Exception("Bad constant <formula> in <constants>. Expected 'name = formula'");

                                    formulaConstants.Add(s[0].Trim(), s[1].Trim());
                                }
                            }
                        }
                        else if (propNode.Name == "detectors")
                        {
                            formulaDetectors = new string[Convert.ToInt16(propNode.Attributes["count"].Value)];
                            int i = 0;
                            foreach (XmlNode pointNode in propNode)
                                if (pointNode.Name == "point")
                                {
                                    if (i >= formulaDetectors.Length)
                                        throw new Exception("Too many detector points. Check count in <detectors>.");
                                    formulaDetectors[i] = pointNode.FirstChild.Value.Trim();
                                    i++;
                                }
                        }
                    }
                }
            }
        }

        public void SaveToXml(string fileName)
        {
            File.WriteAllText(fileName, SaveToXml());
        }

        public string SaveToXml()
        {
            string pointTemplate = "         <point>{0}</point>\r\n";
            string points = "";
            for (int i = 0; i < formulaDetectors.Length; i++)
                points += String.Format(pointTemplate, formulaDetectors[i]);

            string constsTemplate = "         <formula>{0} = {1}</formula>\r\n";
            string consts = "";
            foreach (KeyValuePair<string, string> pair in formulaConstants)
                consts += String.Format(constsTemplate, pair.Key.Trim(), pair.Value.Trim());

            string mainTemplate =
                "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n" +
                "<quantumfractal version=\"2\">\r\n" +
                "   <properties>\r\n" +
                "      <name>{0}</name>\r\n" +
                "      <type>{8}</type>\r\n" +
                "      <description>{1}</description>\r\n" +
                "      <author>{2}</author>\r\n" +
                "      <creationdate>{3}</creationdate>\r\n" +
                "      <alpha>{4}</alpha>\r\n" +
                "      <constants>\r\n" +
                "{7}" +
                "      </constants>\r\n" +
                "      <detectors count=\"{5}\">\r\n" +
                "{6}" +
                "      </detectors>\r\n" +
                "   </properties>\r\n" +
                "</quantumfractal>\r\n";
            return String.Format(mainTemplate, name, description, author, creationDate,
                formulaAlpha, detectors.Length, points, consts, type);
        }

        public void SaveToCsv(string fileName, char separator, string cultureName, int timeCounter)
        {
            if (cultureName == "")
                cultureName = "en-US";
            NumberFormatInfo nfi = new CultureInfo(cultureName, false).NumberFormat;
            using (StreamWriter sw = new StreamWriter(fileName))
            {
                sw.WriteLine("X" + separator + "Y" + separator + "Z");
                for (int i = 0; i < timeCounter; i++)
                {
                    QPoint p = CalculateNextPoint();
                    sw.WriteLine(p.X.ToString(nfi) + separator + 
                        p.Y.ToString(nfi) + separator + p.Z.ToString(nfi));
                }
            }
        }

        /*public void LoadFromCsv(string fileName, char separator, string cultureName)
        {
            if (cultureName == "")
                cultureName = "en-US";
            NumberFormatInfo nfi = new CultureInfo(cultureName, false).NumberFormat;
            using (StreamReader sr = new StreamReader(fileName)) 
            {
                string line;
                while ((line = sr.ReadLine()) != null) 
                {
                    string[] p = line.Split(separator);
                    double x = Convert.ToDouble(p[0].Trim(), nfi);
                    double y = Convert.ToDouble(p[1].Trim(), nfi);
                    double z = Convert.ToDouble(p[2].Trim(), nfi);
                    CalculateNextPoint(x, y, z);
                }
            }
        }*/
    }
}
